package com.ut.richpath.pathparser;

import com.ut.richpath.util.JsonUtil;
import com.ut.richpath.util.LogUtil;
import ohos.agp.render.Path;
import ohos.agp.utils.TextTool;

import java.util.ArrayList;
import java.util.List;

public class PathParserCompat {

    public static class ExtractFloatResult{

        private int endPosition = 0;
        private boolean isEndWithNegOrDot = false;

        public ExtractFloatResult() {}

        /**
         *
         * @param endPosition the position of the next separator
         * @param isEndWithNegOrDot whether the next float starts with a '-' or a '.'
         */
        public ExtractFloatResult(int endPosition, boolean isEndWithNegOrDot) {
            this.endPosition = endPosition;
            this.isEndWithNegOrDot = isEndWithNegOrDot;
        }
    }

    // Copy from Arrays.copyOfRange() which is only available from API level 9.
    /**
     * Copies elements from {@code original} into a new array, from indexes start (inclusive) to
     * end (exclusive). The original order of elements is preserved.
     * If {@code end} is greater than {@code original.length}, the result is padded
     * with the value {@code 0.0f}.
     *
     * @param original the original array
     * @param start    the start index, inclusive
     * @param end      the end index, exclusive
     * @return the new array
     * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
     * @throws IllegalArgumentException       if {@code start > end}
     * @throws NullPointerException           if {@code original == null}
     */
    public static List<Float> copyOfRange(List<Float>  original, int start, int end){
        if (start > end)throw new IllegalArgumentException();
        if (original == null)throw new NullPointerException();
        int originalLength = original.size();
        if (start < 0 || start > originalLength) {
            throw new ArrayIndexOutOfBoundsException();
        }
        int resultLength = end - start;
        int copyLength = Math.min(resultLength, originalLength - start);
        List<Float> result = new ArrayList<>(resultLength);
        System.arraycopy(original, start, result, 0, copyLength);
        return result;
    }

    /**
     * @param pathData The string representing a path, the same as "d" string in svg file.
     * @return the generated Path object.
     */
    public static Path createPathFromPathData(String pathData) {
        Path path = new Path();
        createPathFromPathData(path, pathData);
        return path;
    }

    public static void createPathFromPathData(Path path, String pathData) {
        List<PathDataNode> pathDataNodeList = createNodesFromPathData(pathData);
        if (pathDataNodeList!=null){
            try {
                PathDataNode.nodesToPath(pathDataNodeList, path);
            } catch (RuntimeException e) {
                LogUtil.i("Error in parsing $pathData：%s", e.getLocalizedMessage());
                throw new RuntimeException("Error in parsing $pathData", e);
            }
        }
    }

    /**
     * @param pathData The string representing a path, the same as "d" string in svg file.
     * @return an array of the PathDataNode.
     */
    public static List<PathDataNode> createNodesFromPathData(String pathData){
        if (pathData == null) return null;

        int start = 0, end = 1;
        List<PathDataNode> list = new ArrayList<>();
        while (end < pathData.length()) {
            end = nextStart(pathData, end);
            String s = pathData.substring(start, end).trim();
//            LogUtil.i("start: %d, end: %d, s: %s", start, end, s);
            if (!TextTool.isNullOrEmpty(s)) {
                addNode(list, s.charAt(0), getFloats(s));
            }
            start = end;
            end++;
        }
        if ((end - start) == 1 && start < pathData.length()) {
            addNode(list, pathData.charAt(start), new ArrayList<>());
        }
        return list;
    }

    /**
     * @param source The array of PathDataNode to be duplicated.
     * @return a deep copy of the <code>source</code>.
     */
    public static List<PathDataNode> deepCopyNodes(List<PathDataNode> source){
        List<PathDataNode> copy = new ArrayList<>();
        for (int i=0; i<source.size(); i++) {
            copy.add(i, new PathDataNode(source.get(i)));
        }
        return copy;
    }

    /**
     * @param nodesFrom The source path represented in an array of PathDataNode
     * @param nodesTo   The target path represented in an array of PathDataNode
     * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
     */
    public static boolean canMorph(List<PathDataNode> nodesFrom, List<PathDataNode> nodesTo) {
        if (nodesFrom == null || nodesTo == null) {
            return false;
        }

        if (nodesFrom.size() != nodesTo.size()) {
            return false;
        }

        for (int i=0; i<nodesFrom.size(); i++) {
            if (nodesFrom.get(i).getType() != nodesTo.get(i).getType()
                    || nodesFrom.get(i).getParams().size() != nodesTo.get(i).getParams().size()) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param nodes paths represented in an array of an array of PathDataNode
     * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
     */
    public static boolean canMorph(List<List<PathDataNode>> nodes){
        for (List<PathDataNode> pathDataNode : nodes) {
            if (pathDataNode.isEmpty()) {
                return false;
            }
        }

        for (int i=0; i<nodes.size()-1; i++) {
            if (nodes.get(i).size() != nodes.get(i + 1).size()) {
                return false;
            }
        }

        for (int i=0; i<nodes.size()-1; i++) {
            for (int j=0; j<nodes.get(i).size(); j++) {
                if (nodes.get(i).get(j).getType() != nodes.get(i + 1).get(j).getType()
                        || nodes.get(i).get(j).getParams().size() != nodes.get(i + 1).get(j).getParams().size()) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Update the target's data to match the source.
     * Before calling this, make sure canMorph(target, source) is true.
     *
     * @param target The target path represented in an array of PathDataNode
     * @param source The source path represented in an array of PathDataNode
     */
    public static void updateNodes(List<PathDataNode> target, List<PathDataNode> source) {
        for (int i=0; i<source.size(); i++) {
            target.get(i).setType(source.get(i).getType());
            for (int j=0; i<source.get(i).getParams().size(); j++) {
                target.get(i).getParams().set(j, source.get(i).getParams().get(j));
            }
        }
    }

    private static int nextStart(String s, int end){
        char c;
        int lEnd = end;
        while (lEnd < s.length()) {
            c = s.charAt(lEnd);
            // Note that 'e' or 'E' are not valid path commands, but could be
            // used for floating point numbers' scientific notation.
            // Therefore, when searching for next command, we should ignore 'e'
            // and 'E'.
            if (((c - 'A') * (c - 'Z') <= 0 || (c - 'a') * (c - 'z') <= 0) && c != 'e' && c != 'E') {
                return lEnd;
            }
            lEnd++;
        }
        return lEnd;
    }

    private static void addNode(List<PathDataNode> list, char cmd, List<Float>params) {
        LogUtil.i("command: %c, params: %s", cmd, JsonUtil.toJson(params));
        list.add(new PathDataNode(cmd, params));
    }

    /**
     * Parse the floats in the string.
     * This is an optimized version of parseFloat(s.split(",|\\s"));
     *
     * @param s the string containing a command and list of floats
     * @return array of floats
     */
    private static List<Float> getFloats(String s){
        if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
            return new ArrayList<>(0);
        }
        try {
            List<Float> results = new ArrayList<>();
            int count = 0;
            int startPosition = 1;
            int endPosition;

            ExtractFloatResult result = new ExtractFloatResult();
            int totalLength = s.length();

            // The startPosition should always be the first character of the
            // current number, and endPosition is the character after the current
            // number.
            while (startPosition < totalLength) {
                extract(s, startPosition, result);
                endPosition = result.endPosition;
//                LogUtil.i("start position: %d, end position: %d", startPosition, endPosition);
                if (startPosition < endPosition) {
                    results.add(count++, Float.parseFloat(s.substring(startPosition, endPosition)));
                }

                // Keep the '-' or '.' sign with next number.
                startPosition = result.isEndWithNegOrDot ? endPosition : endPosition + 1;
            }
            return results;
//            return copyOfRange(results, 0, count);
        } catch (NumberFormatException e) {
            throw new RuntimeException("error in parsing $s", e);
        }
    }

    /**
     * Calculate the position of the next comma or space or negative sign
     *
     * @param s      the string to search
     * @param start  the position to start searching
     * @param result the result of the extraction, including the position of the
     *               the starting position of next number, whether it is ending with a '-'.
     */
    private static void extract(String s, int start, ExtractFloatResult result) {
        // Now looking for ' ', ',', '.' or '-' from the start.
        int currentIndex = start;
        boolean foundSeparator = false;
        result.isEndWithNegOrDot = false;
        boolean secondDot = false;
        boolean isExponential = false;
        while (currentIndex < s.length()) {
            boolean isPrevExponential = isExponential;
            isExponential = false;
            char currentChar = s.charAt(currentIndex);
            if (currentChar==' '||currentChar==','){
                foundSeparator = true;
            }else if (currentChar=='-'){
                // The negative sign following a 'e' or 'E' is not a separator.
                if (currentIndex != start && !isPrevExponential) {
                    foundSeparator = true;
                    result.isEndWithNegOrDot = true;
                }
            }else if (currentChar=='.'){
                if (!secondDot) {
                    secondDot = true;
                } else {
                    // This is the second dot, and it is considered as a separator.
                    foundSeparator = true;
                    result.isEndWithNegOrDot = true;
                }
            }else if (currentChar=='e'||currentChar=='E'){
                isExponential = true;
            }
            if (foundSeparator) {
                break;
            }
            currentIndex++;
        }
        // When there is nothing found, then we put the end position to the end
        // of the string.
        result.endPosition = currentIndex;
    }
}
